﻿using System;
using System.IO;
using System.Linq;

using Amazon.S3.Transfer;
using Amazon.S3.Model;
using Amazon.S3;
using System.Collections.Generic;

namespace Lucene.Net.Store.S3 {

	public class Directory : Lucene.Net.Store.Directory {
		private const Int32 bufferSize = 4092;
		private readonly String accessKey;
		private readonly String secretKey;
		private readonly String bucket;
		private readonly AmazonS3 client;
		private Lucene.Net.Store.Directory cacheDirectory;

		public Directory(String accessKey, String secretKey)
			: this(accessKey, secretKey, String.Empty, null) {
		}

		public Directory(String accessKey, String secretKey, String bucketName)
			: this(accessKey, secretKey, bucketName, null) {
		}

		public Directory(String accessKey, String secretKey, Lucene.Net.Store.Directory cacheDirectory)
			: this(accessKey, secretKey, String.Empty, cacheDirectory) {
		}

		public Directory(String accessKey, String secretKey, String bucketName, Lucene.Net.Store.Directory cacheDirectory) {
			this.accessKey = accessKey;
			this.secretKey = secretKey;
			if (String.IsNullOrWhiteSpace(bucketName)) {
				this.bucket = "lucene";
			}
			else {
				this.bucket = bucketName.ToLower();
			}

			this.client = Amazon.AWSClientFactory.CreateAmazonS3Client(this.accessKey, this.secretKey);

			using (ListBucketsResponse response = this.client.ListBuckets()) {
				if (!response.Buckets.Where(b => b.BucketName == this.bucket).Any()) {
					PutBucketRequest request = new PutBucketRequest();
					request.BucketName = this.bucket;
					this.client.PutBucket(request);
					// TODO: Check response?
				}
			}

			this.InitializeCacheDirectory(cacheDirectory);
		}

		private void InitializeCacheDirectory(Lucene.Net.Store.Directory cacheDirectory) {
			// Any cache directory set?
			if (cacheDirectory != null) {
				// Yes, let's store it!
				this.cacheDirectory = cacheDirectory;
			}
			else {
				// No cache directory set, then we'll have to create a local (on the file system) cache.
				String cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "S3Directory");
				DirectoryInfo s3Dir = new DirectoryInfo(cachePath);
				if (!s3Dir.Exists) {
					s3Dir.Create();
				}

				String bucketPath = Path.Combine(cachePath, this.bucket);
				DirectoryInfo bucketDirectory = new DirectoryInfo(bucketPath);
				if (!bucketDirectory.Exists) {
					bucketDirectory.Create();
				}

				this.cacheDirectory = FSDirectory.Open(bucketDirectory);
			}
		}

		/// <summary>
		/// Closes the store.
		/// </summary>
		public override void Close() { }

		public override Lucene.Net.Store.IndexOutput CreateOutput(String name) {
			return new IndexOutput(this, name);
		}

		internal void WriteFile(String name, StreamInput input) {
			Directory.WriteFile(this.client, this.bucket, name, input);
		}

		private static void WriteFile(AmazonS3 client, String bucket, String name, StreamInput stream) {
			PutObjectRequest request = new PutObjectRequest()
				.WithBucketName(bucket)
				.WithKey(name);
			request.WithInputStream(stream);
			using (PutObjectResponse response = client.PutObject(request)) {
			}
		}

		internal void ReadFile(String name, StreamOutput output) {
			Directory.ReadFile(this.client, this.bucket, name, output);
		}

		private static void ReadFile(AmazonS3 client, String bucket, String name, StreamOutput output) {
			GetObjectRequest request = new GetObjectRequest()
				.WithBucketName(bucket)
				.WithKey(name);
			using (GetObjectResponse response = client.GetObject(request)) {
				Byte[] buffer = new Byte[bufferSize];
				Int32 offset = 0;
				Int32 size = response.ResponseStream.Read(buffer, offset, bufferSize);
				while (size == bufferSize) {
					output.Write(buffer, offset, size);
					offset += bufferSize;
					size = response.ResponseStream.Read(buffer, offset, 4092);
				}
				if (size > 0) {
					output.Write(buffer, offset, size);
				}
			}
		}

		//private static Stream ReadFile(String accessKey, String secretKey, String bucket, String file) {
		//    MemoryStream output = new MemoryStream();
		//    using (AmazonS3 client = Amazon.AWSClientFactory.CreateAmazonS3Client(accessKey, secretKey)) {
		//        GetObjectRequest downloadRequest = new GetObjectRequest().WithBucketName(bucket).WithKey(file);
		//        using (S3Response response = client.GetObject(downloadRequest)) {
		//            using (Stream s = response.ResponseStream) {
		//                Byte[] data = new Byte[32768];
		//                Int32 bytesRead = 0;
		//                do {
		//                    bytesRead = s.Read(data, 0, data.Length);
		//                    output.Write(data, 0, bytesRead);
		//                }
		//                while (bytesRead > 0);
		//                output.Flush();
		//            }
		//        }
		//    }

		//    return output;
		//}

		/// <summary>
		/// Removes an existing file in the directory.
		/// </summary>
		/// <param name="name">The name of the file.</param>
		public override void DeleteFile(String name) {
			DeleteObjectRequest request = new DeleteObjectRequest().WithBucketName(this.bucket).WithKey(name);
			this.client.DeleteObject(request);
			// TODO: Get and examine response?!?!?!
		}

		public override void Dispose() {
			this.client.Dispose();
		}

		/// <summary>
		/// Returns true if a file with the given name exists.
		/// </summary>
		/// <param name="name">The name of the file.</param>
		/// <returns></returns>
		public override Boolean FileExists(String name) {
			GetObjectMetadataRequest request = new GetObjectMetadataRequest()
					.WithBucketName(this.bucket)
					.WithKey(name);
			try {
				using (GetObjectMetadataResponse response = this.client.GetObjectMetadata(request)) {
					return true;
				}
			}
			catch (AmazonS3Exception ex) {
				if (ex.StatusCode == System.Net.HttpStatusCode.NotFound) {
					return false;
				}
				throw;
			}
		}

		/// <summary>
		/// Returns the length of a file in the directory.
		/// </summary>
		/// <param name="name">The name of the file.</param>
		/// <returns></returns>
		public override Int64 FileLength(String name) {
			GetObjectMetadataRequest request = new GetObjectMetadataRequest()
					.WithBucketName(this.bucket)
					.WithKey(name);
			using (GetObjectMetadataResponse response = this.client.GetObjectMetadata(request)) {
				// TODO: Correct??? Metadata (object) length or file/object length??
				return response.ContentLength;
			}
		}

		/// <summary>
		/// Return the time the named file was last modified.
		/// </summary>
		public override Int64 FileModified(String name) {
			GetObjectMetadataRequest request = new GetObjectMetadataRequest()
					.WithBucketName(this.bucket)
					.WithKey(name);
			using (GetObjectMetadataResponse response = this.client.GetObjectMetadata(request)) {
				return response.LastModified.Ticks;
			}
		}

		public override String[] ListAll() {
			IEnumerable<String> output = new List<String>();
			ListObjectsRequest request = new ListObjectsRequest()
					.WithBucketName(this.bucket);
			using (ListObjectsResponse response = this.client.ListObjects(request)) {
				output = response.S3Objects.Select(o => o.Key);
			}
			return output.ToArray();
		}

		[Obsolete]
		public override String[] List() {
			return this.ListAll();
		}

		public override Lucene.Net.Store.IndexInput OpenInput(String name) {
			try {
				// TODO: Get data off S3
				return new IndexInput(this, name);
			}
			catch (Exception ex) {
				throw new System.IO.FileNotFoundException(name, ex);
			}
		}

		[Obsolete]
		public override void RenameFile(String from, String to) {
			try {
				// Copy the S3 object!
				CopyObjectRequest copyRequest = new CopyObjectRequest()
					  .WithSourceBucket(this.bucket)
					  .WithSourceKey(from)
					  .WithDestinationBucket(this.bucket)
					  .WithDestinationKey(to);
				this.client.CopyObject(copyRequest);

				//Delete the original
				DeleteObjectRequest deleteRequest = new DeleteObjectRequest()
					   .WithBucketName(this.bucket)
					   .WithKey(from);
				this.client.DeleteObject(deleteRequest);

				// Rename files in the cache!
				if (this.cacheDirectory.FileExists(from)) {
					this.cacheDirectory.RenameFile(from, to);
				}
			}
			catch { }
		}

		/// <summary>
		/// Set the modified time of an exising file to now.
		/// </summary>
		/// <param name="name">Name of the file.</param>
		public override void TouchFile(String name) {
			this.cacheDirectory.TouchFile(name);
		}

		internal Lucene.Net.Store.Directory CacheDirectory {
			get {
				return this.cacheDirectory;
			}
		}

		// TODO: Locking with S3 ??? HOW ???
		private Object lockCollectionLock = new Object();
		private Dictionary<String, Lock> lockCollection = new Dictionary<String, Lock>();

		public override Lucene.Net.Store.Lock MakeLock(String name) {
			lock (lockCollectionLock) {
				if (!lockCollection.ContainsKey(name)) {
					lockCollection.Add(name, new Lock(name, this));
				}
				return lockCollection[name];
			}
		}

		public override void ClearLock(String name) {
			lock (lockCollectionLock) {
				if (lockCollection.ContainsKey(name)) {
					lockCollection[name].Release();
				}
			}
			this.cacheDirectory.ClearLock(name);
		}

		public StreamInput OpenCachedInputAsStream(String name) {
			return new StreamInput(this.CacheDirectory.OpenInput(name));
		}

		public StreamOutput CreateCachedOutputAsStream(String name) {
			return new StreamOutput(this.CacheDirectory.CreateOutput(name));
		}
	}
}